/*
 *  Copyright 2008 Mark Ashworth <javameme@gmail.com>.
 * 
 *  Licensed under the GNU General Public License v3 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.gnu.org/licenses/gpl-3.0.html
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package metaphor.persistence.hibernate;

import java.io.Serializable;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.Session;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.hibernate3.HibernateSystemException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Transactional;
import metaphor.persistence.filter.Query;
import metaphor.persistence.filter.SearchCriteria;
import metaphor.persistence.DaoMode;
import metaphor.persistence.*;
import metaphor.persistence.UnsavedValueStrategy;
import metaphor.persistence.UnsavedValueStrategyException;
import metaphor.persistence.filter.Association;
import metaphor.persistence.filter.*;
import metaphor.persistence.filter.SimpleExpression;

/**
 * Hibernate Data Access Object
 * @author Mark Ashworth <javameme@gmail.com>
 * @version 1.0.0
 */
public class HibernateDao extends HibernateDaoSupport implements Dao {
    /* Logger: This variable needs to hide the Commons Logging specified in Spring */
    protected Logger logger = LoggerFactory.getLogger(getClass());
    
    private UnsavedValueStrategy unsavedValueStrategy = null;
    
    /**
     * Constructs the Hibernate Dao and by default specifies the unsaved value 
     * strategy to be ReflectionUnsavedValueStrategy with method signature <i>getId()</>
     */
    public HibernateDao() {
        super();
    }

    /**
     * Sets the unsaved value strategy that the dao should use to determine
     * if an object should be inserted or updated during the persist operation.
     * 
     * @param unsavedValueStrategy The new value
     */
    public void setUnsavedValueStrategy(UnsavedValueStrategy unsavedValueStrategy) {
        this.unsavedValueStrategy = unsavedValueStrategy;
    }
    
    /**
	 * Deletes the object
	 * @param object The object to delete
     * @throws org.springframework.dao.DataAccessException If there was an database error
	 */
    @Transactional(readOnly=false)
    public void delete(Object object) {
        if (object == null) {
            throw new IllegalArgumentException("The object to delete is null");
        }
        getHibernateTemplate().delete(object);
    }

    /**
     * Deletes a list of persistent objects
     * @param objects The objects to delete
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=false)
    public void delete(List<Object> objects) {
        if (objects == null) {
            throw new IllegalArgumentException("The objects to delete is null");
        }
        getHibernateTemplate().deleteAll(objects);
    }
    
    /**
     * Persists the object.<br />
     * 
     * The UnsavedValueStrategy is used to determine if an
     * object should be inserted or updated. By default the 
     * ReflectionUnsavedValueStrategy is used to test the getId() method whether
     * it is not null.
     * 
     * @param object The object to persist
     * @return The persisted object
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=false)
    public Object persist(Object object) {
        if (object == null) {
            throw new IllegalArgumentException("The object to persist is null");
        }
        
        if (unsavedValueStrategy == null) {
//            unsavedValueStrategy = 
//                    new ReflectionUnsavedValueStrategy(
//                        new MethodSignature("getId", new String[0]));
//            logger.info("The unsaved value strategy is null, defaulting to {}", unsavedValueStrategy);
            throw new UnsavedValueStrategyException("No unsaved value strategy specified");
        } 
        
        if (this.unsavedValueStrategy.isNew(object)) {
            logger.debug("The unsaved value strategy reported object as new");
            Serializable id = getHibernateTemplate().save(object);
            logger.debug("The object was saved with an id of {}", id);
            return retrieveById(object.getClass(), id);
        } else {
            logger.debug("The unsaved value strategy declared object as existing");
            if (getHibernateTemplate().contains(object)) {
                logger.debug("The object is contained in the session, calling merge");
                getHibernateTemplate().merge(object);
            } else {
                try {
                    logger.debug("The object is not contained in the session, calling saveOrUpdate");
                    getHibernateTemplate().saveOrUpdate(object);
                } catch (HibernateSystemException e) {
                    if (e.getCause() instanceof NonUniqueObjectException) {
                        /* Seems like the session already contains the object */
                        logger.debug("Received a NonUniqueObjectException, attempting to merge");
                        //getHibernateTemplate().evict(object);
                        object = getHibernateTemplate().merge(object);
                    } else {
                        throw e;
                    }
                }
            }
            return object;
        }    
    }

    /**
     * Persists the XML element to the database
     * @param persistentClass The persistent class that the XML represents
     * @param xml The xml element to persist
     * @return The persisted object
     * @throws java.lang.IllegalArgumentException If the persistentClass or xml is null
     * @throws xperiment.metaphor.persistence.XMLParseDaoException If the XML could not be parsed
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=false)
    public Object persist(Class persistentClass, String xml) {
        
        if (persistentClass == null) {
            throw new IllegalArgumentException("The persistentClass is null");
        }
        if (xml == null) {
            throw new IllegalArgumentException("The xml value is null");
        }

        try {
            DocumentFactory factory = DocumentFactory.getInstance();
            SAXReader reader = new SAXReader(factory);
            Document doc = reader.read(new StringReader(xml));
            Element root = doc.getRootElement();

            Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
            Session dom4jSession = session.getSession(EntityMode.DOM4J);

            Serializable id = dom4jSession.save(persistentClass.getName(), root);
            return retrieveById(persistentClass, id);
        } catch (DocumentException ex) {
            throw new DaoXMLParseException("Unable to parse xml \r\n" + xml, ex);
        }
    }

    @Transactional(readOnly=false)
    public Object persist(Map object) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

     /**
     * Retrieves all the persistent objects
     * @param persistentClass The persistent class to retrieve for
     * @return List<Object>
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=true)
	public List retrieve(Class persistentClass) {
		return retrieve(persistentClass, DaoMode.POJO);
	}
    
    /**
     * Retrieves all the persistent objects
     * @param persistentClass The persistent class to retrieve for
     * @param mode The data access mode
     * @return List<Object>
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=true)
    public List retrieve(Class persistentClass, DaoMode mode) {
        
        Criteria criteria = null;
        Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
        switch (mode) {
            case POJO:
                criteria = session.createCriteria(persistentClass);
                break;
            case XML: 
                criteria = session.getSession(EntityMode.DOM4J).createCriteria(persistentClass);
                break;
            case MAP:
                criteria = session.getSession(EntityMode.DOM4J).createCriteria(persistentClass);
            
            default: throw new DaoQueryException("The dao mode " + mode + " is not implemented");
        }
        
		return criteria.list();
    }
    
    @Transactional(readOnly=true)
    public List retrieve(Query query) {
        if (query == null) {
            throw new IllegalArgumentException("The query is null");
        } else if (query.getPersistentClass() == null) {
            throw new IllegalArgumentException("The query persistent class is null");
        }
        
        DetachedCriteria criteria = DetachedCriteria.forClass(query.getPersistentClass());

        for (Association association : query.getAssociations()) {
            int joinType = CriteriaSpecification.INNER_JOIN;
            switch (association.getJoin()) {
                case INNER_JOIN: joinType = CriteriaSpecification.INNER_JOIN; break;
                case FULL_JOIN: joinType = CriteriaSpecification.FULL_JOIN; break;
                case LEFT_OUTER_JOIN: joinType = CriteriaSpecification.LEFT_JOIN; break;
            }
            criteria.createAlias(association.getPath(), association.getAlias(), joinType);
        }

        for (Order order : query.getOrder()) {
            if (order.isAscending()) {
                criteria.addOrder(org.hibernate.criterion.Order.asc(order.getPropertyName()));
            } else {
                criteria.addOrder(org.hibernate.criterion.Order.desc(order.getPropertyName()));
            }
        }

        for (Expression expression : query.getCriteria()) {
            if (expression instanceof SimpleExpression) {
                
                SimpleExpression e = (SimpleExpression) expression;
                switch(e.getOperator()) {
                    case LIKE: criteria.add(Restrictions.like(e.getFieldName(), e.getOperand())); break;
                    case GT: criteria.add(Restrictions.gt(e.getFieldName(), e.getOperand())); break;
                    case GE: criteria.add(Restrictions.ge(e.getFieldName(), e.getOperand())); break;
                    case LT: criteria.add(Restrictions.lt(e.getFieldName(), e.getOperand())); break;
                    case LE: criteria.add(Restrictions.le(e.getFieldName(), e.getOperand())); break;
                    case EQ: criteria.add(Restrictions.eq(e.getFieldName(), e.getOperand())); break;
                    case NOT: throw new DaoQueryException("The NOT operator is not implemented yet");
                    case ILIKE: criteria.add(Restrictions.ilike(e.getFieldName(), e.getOperand())); break;   
                    default: throw new DaoQueryException("The operator " + e.getOperator() + " is not yet supported");
                }
            } else {
                throw new DaoQueryException("The expression type " + expression + " is not yet supported");
            }
        }
        return getHibernateTemplate().findByCriteria(criteria);
    }
    
    /**
     * Retrieves the persistent objects via a search criteria. Currently only 
     * that subclass NamedObjects are supported, all other classes will return 
     * all the persistent objects but will be paged according to 
     * the search criteria.
     * 
     * @param searchCriteria The search criteria
     * @return List<Object>
     * @throws xperiment.metaphor.persistence.DaoException If there was an exception
     */
    @Transactional(readOnly=true)
    public List retrieve(SearchCriteria searchCriteria) {
      
        if (searchCriteria == null) {
            throw new IllegalArgumentException("The search criteria is null");
        } else if (searchCriteria.getPersistentClass() == null) {
            throw new IllegalArgumentException("The search criteria persistent class is null");
        }
        
        String pattern = searchCriteria.getSearchPattern();
        DetachedCriteria detachedCriteria = DetachedCriteria.forClass(searchCriteria.getPersistentClass());

        /* Associations */
        for (Association association : searchCriteria.getAssociations()) {
            int joinType = CriteriaSpecification.INNER_JOIN;
            switch (association.getJoin()) {
                case INNER_JOIN: joinType = CriteriaSpecification.INNER_JOIN; break;
                case FULL_JOIN: joinType = CriteriaSpecification.FULL_JOIN; break;
                case LEFT_OUTER_JOIN: joinType = CriteriaSpecification.LEFT_JOIN; break;
            }
            detachedCriteria.createAlias(association.getPath(), association.getAlias(), joinType);
        }
        
        /* Criteria */
        if (searchCriteria.getPersistentFields() != null) {

            if (searchCriteria.getPersistentFields().size() > 0) {
                Junction junction = Restrictions.disjunction();
                for (String field : searchCriteria.getPersistentFields()) {
                    junction.add(Restrictions.ilike(field, pattern));
                }
                detachedCriteria.add(junction);
            }
        }
        
        /* Order */
        for (Order order : searchCriteria.getOrderBy()) {
            if (order.isAscending()) { 
                detachedCriteria.addOrder(org.hibernate.criterion.Order.asc(order.getPropertyName()));
            } else {
                detachedCriteria.addOrder(org.hibernate.criterion.Order.desc(order.getPropertyName()));
            }
        }
        
        Criteria criteria = detachedCriteria.getExecutableCriteria(getHibernateTemplate().getSessionFactory().getCurrentSession());
        if (searchCriteria.isPaged()) {
            criteria.setMaxResults(searchCriteria.getPageSize());
            criteria.setFirstResult(searchCriteria.getPage() * searchCriteria.getPageSize());
        }
        return criteria.list();    
    }
    
    /**
     * Retrieves the persistent object by id
     * @param persistentClass The persistent class to retrieve for
     * @param id The id of the persistent object
     * @return Object
     * @throws org.springframework.dao.DataAccessException If there was an database error
     */
    @Transactional(readOnly=true)
    public Object retrieveById(Class persistentClass, Serializable id) {
        Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
		Criteria criteria = session.createCriteria(persistentClass);
		criteria.add(org.hibernate.criterion.Expression.eq("id", id));
		return criteria.uniqueResult();
    }
}
